package sg.edu.nus.comp.cs4218.impl;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

import sg.edu.nus.comp.cs4218.impl.ATool;
import sg.edu.nus.comp.cs4218.extended2.ICutTool;

import java.util.*;

public class CutTool extends ATool implements ICutTool {
	private String fileName;
	private boolean hasDelim;
	private boolean hasList;
	private boolean hasField;
	private boolean isStdin;
	
	/**
	 * Constructor
	 * 
	 * @param args
	 *            - Arguments the tool is going to be executed with.
	 */
	public CutTool(String[] args) {
		super(args);
	}

	/**
	 * First, checks whether commands, options and fileName in args, stdin is valid.
	 * Second, call the cut methods according to options.
	 * 
	 * @param workingDir
	 *            - current working directory
	 * @param stdin
	 *            - user input content (to be used when file is not given)
	 *            
	 * @return Output generated by executing cut command
	 */
	@Override
	public String execute(File workingDir, String stdin) {
		if(args == null){
			setStatusCode(1);
			return "Invalid command";
		}
		if(checkForHelpCommand())
			return getHelp();
		if(!checkArgsValidity(workingDir, stdin)) {
			setStatusCode(1);
			return "Invalid command";
		}
		
		
		// execute command
		return executeCommand(workingDir, stdin);
	}
	

	/**
	 * Cuts characters from input based on positions specified in list.
	 * 
	 * @param list
	 *            - a list of sorted (increasing order, no duplicates, no overlapped values) positions of characters to cut out
	 * @param input
	 *            - current line of the file/stdin
	 *            
	 * @return Characters cut from input
	 */
	@Override
	public String cutSpecfiedCharacters(String list, String input) {

		String result = "";
		StringTokenizer st = new StringTokenizer(list, ",");
		try {
			while (st.hasMoreTokens()) {
				String token = st.nextToken();
				int dashIndex = token.indexOf('-');
				if (dashIndex > 0) {
					// a range of numbers
					int startIndex = Integer.parseInt(token.substring(0,
							dashIndex));
					int endIndex = Integer.parseInt(token.substring(
							dashIndex + 1, token.length()));
					if(endIndex > input.length())
						endIndex = input.length();
					result += input.substring(startIndex - 1, endIndex);

				} else {
					// single digit
					int startIndex = Integer.parseInt(token);
					result += input.substring(startIndex - 1, startIndex);
				}
			}
		} catch (Exception e) {
			setStatusCode(1);
			return "Invalid command";
		}
		setStatusCode(0);
		return result;
	}

	/**
	 * Delimit input using delim then return characters based on delimited input and positions
	 * specified in the list
	 * 
	 * @param list
	 *            - a list of sorted (increasing order, no duplicates, no overlapped values) positions of characters to cut out
	 * @param delim
	 *            - delimit the line by this delim instead of default space
	 * @param input
	 *            - current line of the file/stdin
	 *            
	 * @return Characters in specific positions of input after input is delimited by delim
	 */
	@Override
	public String cutSpecifiedCharactersUseDelimiter(String list, String delim,
			String input) {
		String result = "";
			
		StringTokenizer st;
		List<String> rangeList =  fillRangeList(list);
		int index = 1;
		int listCounter = 0;
		String range = rangeList.get(listCounter);
		int num = -1, startNum = -1, endNum = -1;

		try {
			st = new StringTokenizer(input, delim);
			while (st.hasMoreTokens()) {
				String currentWord = st.nextToken();
				// move to next element in rangeList
				
				if (listCounter < rangeList.size()) {
					range = rangeList.get(listCounter);
					if (range.indexOf("-") > 0) {
						startNum = Integer.parseInt(range
								.substring(0, range.indexOf("-")));
						endNum = Integer.parseInt(range.substring(
								range.indexOf("-") + 1,
								range.length()));
						num = -1;
					} else {
						num = Integer.parseInt(range);
						if (num <= 0) {
							setStatusCode(1);
							return "";
						}
						startNum = -1;
						endNum = -1;
					}			
				}
				listCounter++;
				// first condition is for num, second condition is for range
				if (index == num || (index >= startNum && index <= endNum)) {
					if (result.length() > 0) 
						result += delim;
					result += currentWord;
				} 
				index++;
			}
		} catch (NumberFormatException e) {
			setStatusCode(1);
			return "Invalid command";
		}
		setStatusCode(0);
		return result;
	}

	/**
	 * Display help for cut command.
	 * 
	 * @return Options available for cut command
	 */
	@Override
	public String getHelp() {
		return "cut : prints a substring that is specified in a certain range"
				+ "\nCommand Format - cut [OPTIONS] [FILE]"
				+ "\nFILE - Name of the file, when no file is present (denoted by '-') use standard input OPTIONS"
				+ "\n-c LIST: Use LIST as the list of characters to cut out. E.g 'cut -c 1-5,10,18-30'"
				+ "\nspecifies characters 1 through 5, 10 and 18 through 30."
				+ "\n-d DELIM -f FIELD: Use DELIM as the field-separator character instead of the TAB character."
				+ "\nFIELD is similar to LIST except FIELD is used as the list of string to cut out after delimited by DELIM."
				+ "\nThe way to define FIELD is the same as LIST."
				+ "\n-help : Brief information about supported options";
	}

	// Helper methods
	/**
	 * First, checks whether commands, options and fileName in args, stdin is valid.
	 * Second, call the cut methods according to options.
	 * 
	 * @param workingDir
	 *            - current working directory
	 * @param stdin
	 *            - user input content (to be used when file is not given)
	 *            
	 * @return Output generated by executing cut command
	 */
	private String executeCommand(File workingDir, String stdin) {
		String result = "";
		for (int i = 0; i < args.length; i++) {
			String tempStr = args[i];
			if (tempStr.equals("-c")) {
				if (i + 2 < args.length) {
					// sort and remove duplicated numbers in range
					args[i + 1] = sortAndRemoveDuplicateNumbers(args[i + 1]);
					if (args[i + 1].equals("Invalid command")) {
						setStatusCode(1);			
						break;
					}
					result = cutSpecfiedCharacters(workingDir, stdin, isStdin, args[i+1]);
					i+=1;
				} else {
					setStatusCode(1);
					break;
				}		
			} else if (tempStr.equals("-d")) {
				if (i + 4 < args.length) {
					if (args[i + 2].equals("-f")) {
						// sort and remove duplicated numbers in range
						args[i + 3] = sortAndRemoveDuplicateNumbers(args[i + 3]);
						if (args[i + 3].equals("Invalid command")) {
							setStatusCode(1);
							break;
						}
						result = cutSpecifiedCharactersUseDelimiter(workingDir, stdin, isStdin, args[i+3], args[i+1]);
						i+=3;
					}
				} else {
					setStatusCode(1);
					break;
				}		
			} else if (tempStr.equals("-f")) {
				if (i + 4 < args.length) {
					if (args[i + 2].equals("-d")) {
						// sort and remove duplicated numbers in range
						args[i + 1] = sortAndRemoveDuplicateNumbers(args[i + 1]);
						if (args[i + 1].equals("Invalid command")) {
							setStatusCode(1);						
							break;
						}
						result = cutSpecifiedCharactersUseDelimiter(workingDir, stdin, isStdin, args[i+1], args[i+3]);
						i += 3;
					}
				} else {
					setStatusCode(1);
					break;
				}
			}
		}
		if(getStatusCode() == 1)
			return "Invalid command";
		else
			return result;
	}
		
	/**
	 * Sorts the list in ascending order and remove duplicates and overlapped values.
	 * 
	 * @param list
	 *            - a list of valid range/digit which may be unsorted and contain duplicates, overlaps
	 * @return Sorted (ascending order) and unique values of list
	 */
	public String sortAndRemoveDuplicateNumbers(String list) {
		String result = "";
		List<String> rangeList =  fillRangeList(list);

		// sort the list
		Collections.sort(rangeList);

		try {
			// if there's only 1 element in rangeList
			if (rangeList.size() == 1) {
				String temp = rangeList.get(0);
				if (temp.contains("-")) {
					int dashIndex = temp.indexOf("-");
					Integer.parseInt(temp.substring(0, dashIndex));
					Integer.parseInt(temp.substring(dashIndex + 1,
							temp.length()));
				} else 
					Integer.parseInt(temp);		
			}
			// if there's > 1 element in rangeList
			sortAndRemoveDuplicateNumbers(rangeList);
		} catch (NumberFormatException e) {
			setStatusCode(1);
			return "Invalid command";
		}
		
		for (int i = 0; i < rangeList.size(); i++) {
			result += rangeList.get(i);
			if (i < rangeList.size() - 1)
				result += ",";
		}
		setStatusCode(0);
		return result;
	}
	/**
	 * (only for rangeList.size() > 1) Sorts the list in ascending order and remove duplicates and overlapped values.
	 * 
	 * @param list
	 *            - a list of valid range/digit which may be unsorted and contain duplicates, overlaps
	 * @return Sorted (ascending order) and unique values of list
	 */
	private void sortAndRemoveDuplicateNumbers(List<String> rangeList) {
		int j;
		for (int i = 0; i < rangeList.size(); i++) {
			j = i + 1;
			if (j < rangeList.size()) {
				String first = rangeList.get(i);
				String second = rangeList.get(j);
	
				if (first.equals(second)) {
					rangeList.remove(i); // remove one of the duplicates
					i--;
				} else {
					// both are ranges
					if (first.contains("-") && second.contains("-")){				
						int firstStartNum = getStartNumInRange(first);
						int firstEndNum = getEndNumInRange(first);
						int secondStartNum = getStartNumInRange(second);
						int secondEndNum = getEndNumInRange(second);
						
						if (firstStartNum == secondStartNum) {
							// firstEndNum < secondEndNum is always true since already do Collections.sort
							rangeList.remove(i);
							i--;
						} 	
						else { // firstStartNum < secondStartNum is always true since already do Collections.sort
							if (firstEndNum < secondEndNum
									&& firstEndNum >= secondStartNum) {
								rangeList.remove(i);
								i--;
								rangeList.set(j - 1, firstStartNum
										+ "-" + secondEndNum);
							} else 
								rangeList.remove(j);		
						} 					
					} else { // either but not both is range
						String range, digit;
						// first is digit and second is range
						if (first.contains("-")) {
							range = first;
							digit = second;
						} else {	// second is digit and first is range
							range = second;
							digit = first;
						}
						int rangeStartNum = getStartNumInRange(range);
						int rangeEndNum = getEndNumInRange(range);
						int num = Integer.parseInt(digit);
						if (num >= rangeStartNum && num <= rangeEndNum) {
							if(second.contains("-")){
								rangeList.remove(i);
								i--;
							} else
								rangeList.remove(j);
						}		
					}
				}
			}
		}	// end of for
	}

	
	/**
	 * get start number of a given range
	 * 
	 * @param str
	 *            - range of digits
	 * @return returns start number of range
	 */
	private int getStartNumInRange(String str) {
		return Integer.parseInt(str.substring(0, str.indexOf("-")));
	}
	/**
	 * get end number of a given range
	 * 
	 * @param str
	 *            - range of digits
	 * @return returns end number of range
	 */
	private int getEndNumInRange(String str) {
		return Integer.parseInt(str.substring(str.indexOf("-") + 1, str.length()));
	}
	/**
	 * copy each element from the given string of list into a rangeList
	 * 
	 * @param list
	 *            - contains a string of elements
	 * @return returns rangeList filled
	 */
	private List<String> fillRangeList(String list) {
		List<String> rangeList = new ArrayList<String>();
		StringTokenizer st = new StringTokenizer(list, ",");
		while (st.hasMoreTokens()) {
			rangeList.add(st.nextToken());
		}
		return rangeList;
	}

	/**
	 * Opens file or scans stdin line by line 
	 * and get output from cutSpecifiedCharactersUseDelimiter method
	 * 
	 * @param workingDir
	 *            - current working directory
	 * @param stdin
	 *            - user input content (to be used when file is not given)
	 * @param isStdin
	 *            - whether it is stdin or read from fileName
	 * @param list
	 *            - list of positions to be cut after delimited by delim
	 * @param delim
	 *            - delim used to delimit line
	 *            
	 * @return concentrated results from executing cutSpecifiedCharactersUseDelimiter method
	 */
	private String cutSpecifiedCharactersUseDelimiter(File workingDir, String stdin, boolean isStdin, String list, String delim) {
		String result = "";
		if(isStdin) {
			result = cutSpecifiedCharactersUseDelimiterForStdin(stdin, list,
					delim);
		} else {
			result = cutSpecifiedCharacterUseDelimiterForFile(workingDir, list,
					delim);
		}
		setStatusCode(0);
		return result;
	}

	/**
	 * Opens file and read line by line,
	 * get output from cutSpecifiedCharactersUseDelimiter method
	 * 
	 * @param workingDir
	 *            - current working directory
	 * @param list
	 *            - list of positions to be cut after delimited by delim
	 * @param delim
	 *            - delim used to delimit line
	 *            
	 * @return concentrated results from executing cutSpecifiedCharactersUseDelimiter method
	 */
	private String cutSpecifiedCharacterUseDelimiterForFile(File workingDir,
			String list, String delim) {
		String result = "";
		String currentLine;
		BufferedReader br = null;
		// open file
		try {
			br = new BufferedReader(new FileReader(
					workingDir + File.separator + fileName));

			while ((currentLine = br.readLine()) != null) {
				if (result.length() > 0) {
					result += "\n";
				}
				result += cutSpecifiedCharactersUseDelimiter(list, delim,
						currentLine);
			}
		} catch (IOException e) {
			e.printStackTrace();
			setStatusCode(1);
			return "";
		} finally {				
			try {
				if (br != null)
					br.close();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		}
		return result;
	}

	/**
	 * Scans stdin line by line 
	 * and get output from cutSpecifiedCharactersUseDelimiter method
	 * 

	 * @param stdin
	 *            - user input content (to be used when file is not given)
	 * @param list
	 *            - list of positions to be cut after delimited by delim
	 * @param delim
	 *            - delim used to delimit line
	 *            
	 * @return concentrated results from executing cutSpecifiedCharactersUseDelimiter method
	 */
	private String cutSpecifiedCharactersUseDelimiterForStdin(String stdin,
			String list, String delim) {
		String result = "";
		StringTokenizer st = new StringTokenizer(stdin, "\n");
		while(st.hasMoreTokens()){
			if (result.length() > 0) {
				result += "\n";
			}
			result += cutSpecifiedCharactersUseDelimiter(
					list, delim,
					st.nextToken());
		}
		return result;
	}
	
	
	/**
	 * Opens file or scans stdin line by line 
	 * and get output from cutSpecfiedCharacters method
	 * 
	 * @param workingDir
	 *            - current working directory
	 * @param stdin
	 *            - user input content (to be used when file is not given)
	 * @param isStdin
	 *            - whether it is stdin or read from fileName
	 * @param list
	 *            - list of positions to be cut
	 *            
	 * @return concentrated results from executing cutSpecfiedCharacters method
	 */
	private String cutSpecfiedCharacters(File workingDir, String stdin, boolean isStdin, String list) {
		String result = "";
		String currentLine = "";
		if(isStdin) {
			StringTokenizer st = new StringTokenizer(stdin, "\n");
			while(st.hasMoreTokens()){
				if (result.length() > 0) {
					result += "\n";
				}
				result += cutSpecfiedCharacters(list,
						st.nextToken());
			}
		} else {		
			BufferedReader br = null;
			// open file
			try {
				br = new BufferedReader(new FileReader(workingDir
						+ File.separator + fileName));

				while ((currentLine = br.readLine()) != null) {
					if (result.length() > 0) {
						result += "\n";
					}
					result += cutSpecfiedCharacters(list,
							currentLine);
				}
			} catch (IOException e) {
				e.printStackTrace();
				setStatusCode(1);
				return "";
			} finally {
				try {
					if (br != null)
						br.close();
				} catch (IOException ex) {
					ex.printStackTrace();
				}
			}		
		}
		setStatusCode(0);
		return result;
	}
	
	/**
	 * Checks -c cannot have -f and -d and needs to have list
	 * Checks -d must have delim and -f then field number 
	 * If fileName is - it checks that stdin is not null. 
	 * If fileName is not -, it checks that file exists and is not a directory
	 * 
	 * @param workingDir 
	 *            - current working directory
	 * @param stdin
	 *            - user input content (to be used when file is not given)
	 * @return returns false if the above criteria aren't met, else true
	 */
	private boolean checkArgsValidity(File workingDir, String stdin) {
		// check whether commands and options are valid
		if (args.length == 0) {
			setStatusCode(1);
			return false;
		}

		for (int i = 0; i < args.length; i++) {
			if (args[i].equals("-c")) {
				if (hasDelim || hasList || hasField)
					return false;
				i += 1;
				hasList = true;
			} else if (args[i].equals("-d")) {
				if (hasDelim || hasList) 
					return false;
				i += 1;
				hasDelim = true;
			} else if (args[i].equals("-f")) {
				if (hasField || hasList)
					return false;
				i += 1;
				hasField = true;
			} 
			// get file name
			else {
				if (hasField && hasDelim && !hasList || hasList
						&& !(hasField && hasDelim)) {					
					if (args[i].equals("-")) {				
						if(stdin == null)
							return false;				
						isStdin = true;
					}
					else {
						fileName = args[i];
						File f = new File(workingDir + File.separator + fileName);
						if (!f.exists() || f.isDirectory()) 
							return false;
					}
				} else
					return false;
			}
		}	
		return true;
	}
	
	/**
	 * Checks whether -help exists in args
	 * 
	 * @return returns true if -help exists, otherwise false
	 */
	private boolean checkForHelpCommand() {
		for (int i = 0; i < args.length; i++) {
			if (args[i].equals("-help")) {
				return true;
			} 
		}
		return false;
	}
}
